C 26-37 指针与数组
26 指针的本质分析
程序中的变量只是一段存储空间的别名,那么是不是必须通过这个别名才能使用这段存储空间?
下面的程序输出什么?为什么?
int i=5;
int *p = &i;
printf("%d,%p\n",i,p);
*p = 10;
printf("%d,%p\n",i,p);
- 在指针声明时,*号表示所声明的变量为指针
- 在指针使用时,*号表示取指针所指向的内存空间中的值
int i=0;
int j=0;
//指针声明
int *p=&i;
//取值
j = *p;
*号类似一把钥匙,通过这把钥匙可以打开内存,读取内存中的值。
- 小贴士
int i = 0;
int j = 0;
int *p = &i;
j = *p;
/*变量p保存着变量i的内存地址,即:
p<-->&i
*p<-->i
*/
实例分析
- 指针使用示例
#include <stdio.h>
int main()
{
int i = 0;
int* pI;
char* pC;
float* pF;
pI = &i;
*pI = 10;
printf("%p, %p, %d\n", pI, &i, i);
printf("%d, %d, %p\n", sizeof(int*), sizeof(pI), &pI);
printf("%d, %d, %p\n", sizeof(char*), sizeof(pC), &pC);
printf("%d, %d, %p\n", sizeof(float*), sizeof(pF), &pF);
return 0;
}
- 指针是变量,因此可以声明指针参数
- 当一个函数体内部需要改变实参的值,则需要使用指针参数
- 函数调用时实参值将复制到形参
- 指针适用于复杂数据类型作为参数的函数中
编程实验
- 利用指针交换变量
#include <stdio.h>
int swap(int* a, int* b)
{
int c = *a;
*a = *b;
*b = c;
}
int main()
{
int aa = 1;
int bb = 2;
printf("aa = %d, bb = %d\n", aa, bb);
swap(&aa, &bb);
printf("aa = %d, bb = %d\n", aa, bb);
return 0;
}
- const int *p //p可变,p指向的内容不可变
- int const *p; //p可变,p指向的内容不可变
- int * const p; //p不可变,p指向的内容可变
- const int * const p; //p和p指向的内容都不可变
口诀:左数右指 当const出现在*号左边时指针指向的数据为常量,当const出现在*号右边时指针本身为常量。
实例分析
- 常量与指针
#include <stdio.h>
int main()
{
int i = 0;
const int* p1 = &i;
int const* p2 = &i;
int* const p3 = &i;
const int* const p4 = &i;
*p1 = 1; // compile error
p1 = NULL; // ok
*p2 = 2; // compile error
p2 = NULL; // ok
*p3 = 3; // ok
p3 = NULL; // compile error
*p4 = 4; // compile error
p4 = NULL; // compile error
return 0;
}
小结
- 指针是C语言中一种特别的变量
- 指针所保存的值是内存的地址
- 可以通过指针修改内存中任意地址内容
27 数组的本质分析
- 数组是相同类型的变量的有序集合
数组的大小
- 数组在一片连续的内存空间中存储元素
- 数组的个数可以显示或隐式指定
int a[5]={1,2};
int b[]={1,2};
问题:
- a[2],a[3],a[4]的值是多少?
- b包含了多少个元素?
编程实验
- 数组的初始化
#include <stdio.h>
int main()
{
int a[5] = {1, 2};
int b[] = {1, 2};
printf("a[2] = %d\n", a[2]);
printf("a[3] = %d\n", a[3]);
printf("a[4] = %d\n", a[4]);
printf("sizeof(a) = %d\n", sizeof(a));
printf("sizeof(b) = %d\n", sizeof(b));
printf("count for a: %d\n", sizeof(a)/sizeof(int));
printf("count for b: %d\n", sizeof(b)/sizeof(int));
return 0;
}
- 数组名代表数组首元素的地址
- 数组的地址需要用取地址符&才能得到
- 数组首元素的地址与数组的地址值相同
- 数组首元素的地址与数组的地址是两个不同的概念
编程实验
- 数组名和数组地址
#include <stdio.h>
int main()
{
int a[5] = { 0 };
printf("a = %p\n", a);
printf("&a = %p\n", &a);
printf("&a[0] = %p\n", &a[0]);
return 0;
}
- 数组名可以看作一个常量指针
- 数组名“指向”的是内存中数组首元素的起始位置
- 数组名不包含数组的长度信息
- 在表达式中数组名只能作为右值使用
- 只有在下列场合中数组名不能看作常量指针
- 数组名作为sizeof操作符的参数
- 数组名作为&运算符的参数
实例分析
- 数组和指针并不相同
#include <stdio.h>
int main()
{
int a[5] = {0};
int b[2];
int* p = NULL;
p = a;
printf("a = %p\n", a);
printf("p = %p\n", p);
printf("&p = %p\n", &p);
printf("sizeof(a) = %d\n", sizeof(a));
printf("sizeof(p) = %d\n", sizeof(p));
printf("\n");
p = b;
printf("b = %p\n", b);
printf("p = %p\n", p);
printf("&p = %p\n", &p);
printf("sizeof(b) = %d\n", sizeof(b));
printf("sizeof(p) = %d\n", sizeof(p));
b = a;
return 0;
}
小结
- 数组是一片连续的内存空间
- 数组的地址和数组首元素的地址意义不同
- 数组名在大多数情况下被当成常量指针处理
- 数组名其实并不是指针,不能将其等同于指针
概念的混淆是BUG的根源之一!
28 指针和数组分析(上)
- 数组是一段连续的内存空间
- 数组的空间大小为sizeof(array_type)*array_size
- 数组名可看作指向数组第一个元素的常量指针
问题:
- a+1的意义是什么?结果是什么?
- 指针运算的意义是什么?结果又是什么?
编程实验
- a+1的结果是什么?
#include <stdio.h>
int main()
{
int a[5] = {0};
int* p = NULL;
printf("a = 0x%X\n", (unsigned int)(a));
printf("a + 1 = 0x%X\n", (unsigned int)(a + 1));
printf("p = 0x%X\n", (unsigned int)(p));
printf("p + 1 = 0x%X\n", (unsigned int)(p + 1));
return 0;
}
- 指针是一种特殊的变量,与整数的运算规则为
p+n; <--> (unsigned int)p+n*sizeof(\*p);
结论:
当指针p指向一个同类型的数组的元素时:p+1将指向当前元素的下一个元素;p-1将指向当前元素的上一个元素
- 指针之间只支持减法运算
- 参与减法运算的指针类型必须相同
p1-p2; <--> (unsigned int)p1-(unsigned int)p2/sizeof(type);
注意:
- 只有当两个指针指向同一个数组的元素时,指针相减才会有意义,其意义为指针所指元素的下标差
- 当两个指针指向的元素不在同一个数组中时,结果未定义
- 指针也可以进行关系运算
<,<=,>,>=
- 指针关系运算的前提是同时指定同一个数组中的元素
- 任意两个指针之间的比较运算(==,!=)无限制
- 参与比较运算的指针类型必须相同
实例分析
- 指针运算初探
#include <stdio.h>
int main()
{
char s1[] = {'H', 'e', 'l', 'l', 'o'};
int i = 0;
char s2[] = {'W', 'o', 'r', 'l', 'd'};
char* p0 = s1;
char* p1 = &s1[3];
char* p2 = s2;
int* p = &i;
printf("%d\n", p0 - p1);
printf("%d\n", p0 + p2);
printf("%d\n", p0 - p2);
printf("%d\n", p0 - p);
printf("%d\n", p0 * p2);
printf("%d\n", p0 / p2);
return 0;
}
- 指针运算的应用
#include <stdio.h>
#define DIM(a) (sizeof(a) / sizeof(*a))
int main()
{
char s[] = {'H', 'e', 'l', 'l', 'o'};
char* pBegin = s;
char* pEnd = s + DIM(s); // Key point
char* p = NULL;
printf("pBegin = %p\n", pBegin);
printf("pEnd = %p\n", pEnd);
printf("Size: %d\n", pEnd - pBegin);
for(p=pBegin; p<pEnd; p++)
{
printf("%c", *p);
}
printf("\n");
return 0;
}
小结
- 数组声明时编译器自动分配一片连续的内存空间
- 指针声明时只分配了用于容纳地址值的4字节空间
- 指针和数组可以进行运算,其结果为指针
- 指针之间只支持减法运算,其结果为数组元素下标差
- 指针之间支持比较运算,其类型必须相同
29 指针和数组分析(下)
数组名可以当作常量指针使用,那么指针是否也可以当作数组名来使用呢?
- 以下标的形式访问数组中的元素
int main(){
int a[5]={0};
a[1]=3;
a[2]=5;
return 0;
}
- 以指针的形式访问数组中的元素
int mian(){
int a[5]={0};
*(a+1)=3;
*(a+2)=5;
return 0;
}
-
指针以固定增量在数组中移动时,效率高于下标形式
-
指针增量为1且硬件具有硬件增量模型时,效率更高
-
下标形式与指针形式的转换
a[n] <--> *(a+n) <--> *(n+a) <--> n[a]
注意:现代编译器的生成代码优化率已大大提高,在固定增量时,下标形式的效率已经和指针形式相当;但从可读性和代码维护的角度来看,下标形式更优。
实例分析
- 数组的访问方式
#include <stdio.h>
int main()
{
int a[5] = {0};
int* p = a;
int i = 0;
for(i=0; i<5; i++)
{
p[i] = i + 1;
}
for(i=0; i<5; i++)
{
printf("a[%d] = %d\n", i, *(a + i));
}
printf("\n");
for(i=0; i<5; i++)
{
i[a] = i + 10;
}
for(i=0; i<5; i++)
{
printf("p[%d] = %d\n", i, p[i]);
}
return 0;
}
- 数组和指针不同
//ext.c
int a[] = {1, 2, 3, 4, 5};
#include <stdio.h>
int main()
{
extern int* a;
printf("&a = %p\n", &a);
printf("a = %p\n", a);
printf("*a = %d\n", *a);
return 0;
}
//会发生段错误,数组名不是指针
#a和&a的区别
-
a为数组首元素的地址
-
&a为整个数组的地址
-
a和&a的区别在于指针运算
a+1
->(unsigned int)a+sizeof(*a)
&a+1
->(unsigned int)(&a)+sizeof(*&a)
->(unsigned int)(&a)+sizeof(a)
实例分析
- 指针运算经典问题
#include <stdio.h>
int main()
{
int a[5] = {1, 2, 3, 4, 5};
int* p1 = (int*)(&a + 1);
int* p2 = (int*)((int)a + 1);
int* p3 = (int*)(a + 1);
printf("%d, %d, %d\n", p1[-1], p2[0], p3[1]);
return 0;
}
// A. 数组下标不能是负数,程序无法运行
// B. p1[-1]将输出随机数,p2[0]输出2, p3[1]输出3
// C. p1[-1]将输出乱码, p2[0]和p3[1]输出2
-
数组作为参数时,编译器将其编译成对应的指针
void f(int a[]); <--> void f(int *a);
void f(int a[5]); <--> void f(int *a);
结论:一般情况下,当定义的函数中有数组参数时,需要定义另一个参数来标示数组的大小
实例分析
- 虚幻的数组参数
#include <stdio.h>
void func1(char a[5])
{
printf("In func1: sizeof(a) = %d\n", sizeof(a));
*a = 'a';
a = NULL;
}
void func2(char b[])
{
printf("In func2: sizeof(b) = %d\n", sizeof(b));
*b = 'b';
b = NULL;
}
int main()
{
char array[10] = {0};
func1(array);
printf("array[0] = %c\n", array[0]);
func2(array);
printf("array[0] = %c\n", array[0]);
return 0;
}
#小结
- 数组名和指针仅使用方式相同
- 数组名的本质不是指针
- 指针的本质不是数组
- 数组名并不是数组的地址,而是数组首元素的地址
- 函数的数组参数退化为指针
30 C语言中的字符串
- 字符串是有序字符的集合
- 字符串是程序中的基本元素之一
- C语言中没有字符串的概念
- C语言通过特殊的字符串数组模拟字符串
- C语言中的字符串是以'\0'结尾的字符数组
- 在C语言中,双引号引用的单个或多个字符是一种特殊的字面量
- 存储于程序的全局只读存储区
- 本质为字符数组,编译器自动在结尾加上'\0'字符
下面哪些是字符串的定义?
char ca[]={'H','e','l','l','o'};
char sa[]={'W','o','r','l','d','0'};
char ss[]="Hello world!";
char *str = "Hello world!";
实例分析
- 字符数组与字符串
#include <stdio.h>
int main()
{
char ca[] = {'H', 'e', 'l', 'l', 'o'};
char sa[] = {'W', 'o', 'r', 'l', 'd', '\0'};
char ss[] = "Hello world!";
char* str = "Hello world!";
printf("%s\n", ca);
printf("%s\n", sa);
printf("%s\n", ss);
printf("%s\n", str);
return 0;
}
- 你知道吗?
- 字符串字面量的本质是一个数组
- 字符串字面量可以看作常量指针
- 字符串字面量中的字符不可改变
- 字符串字面量至少包含一个字符
- "Hello World!"是一个无名的字符数组
下面的表达式正确吗?
char b = "abc"[0];
char c= *("123"+1);
char t = *''';
实例分析
- 字符串字面量的本质
#include <stdio.h>
int main()
{
char b = "abc"[0];
char c = *("123" + 1);
char t = *"";
printf("%c\n", b);
printf("%c\n", c);
printf("%d\n", t);
printf("%s\n", "Hello");
printf("%p\n", "World");
return 0;
}